﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;

namespace CleanSVG
{
	public class SVGCleaner
	{
        private static NamedEntitiesDictionary _ned;
        
        private readonly List<string> _foreignNamespacePrefixes = new List<string>();
		private readonly Dictionary<string, string> _dynamicStyles = new Dictionary<string, string>();

		private readonly Regex rxForeignNamespace = new Regex(@"\s+xmlns:(?!svg|xlink)(?<prefix>\w+)\s*=\s*""[^""]+""",
		                                                      RegexOptions.IgnoreCase | RegexOptions.Compiled);

        private readonly Regex rxIdAttribute = new Regex(@"\s+id=""(?<id>[a-z_][\w\-]*)""", RegexOptions.IgnoreCase | RegexOptions.Compiled);
        private readonly Regex rxLocalIdReference = new Regex(@"#(?<id>[a-z_][\w\-]*)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
        private readonly Regex rxStyleAttribute = new Regex(@"\s+style\s*=\s*""(?<inner>[^""]*)""", RegexOptions.IgnoreCase | RegexOptions.Compiled);

        private readonly Regex rxStyleCdata = new Regex(@"<style\s+type=""text/css"">\s*(?:(?:/\*)?<!\[CDATA\[(?:\*/)?)?\s*(?<inner>.*?)\s*(?:(?:/\*)?\]\]>(?:\*/)?)?\s*</style>",
		                                                RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);

		private readonly Regex rxSvg = new Regex(@"<svg[^>]*>", RegexOptions.Compiled);
		private readonly Regex rxSyntheticTitles = new Regex(@"<title>[a-z_\-]+[0-9\-\.]+</title>", RegexOptions.IgnoreCase | RegexOptions.Compiled);
		private readonly Regex rxMetadataElement = new Regex(@"<metadata[^>]*?/>|<metadata[^>]*>.*?</metadata>",
																  RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);
        private readonly Regex rxFontElement = new Regex(@"<font[^>]*?/>|<font[^>]*>.*?</font>",
                                                                  RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);
        private readonly Regex rxFilterElement = new Regex(@"<filter[^>]*?/>|<filter[^>]*>.*?</filter>",
                                                                  RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);
        private readonly Regex rxTitleElement = new Regex(@"<(?<tag>title)>.*?</\k<tag>>",
                                                                  RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);
        private readonly Regex rxDescElement = new Regex(@"<(?<tag>desc)>.*?</\k<tag>>",
                                                                  RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);
        private readonly Regex rxForeignObjectElement = new Regex(@"<(?<tag>foreignObject)[^>]*>.*?</\k<tag>>",
                                                                  RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);
        private readonly Regex rxSwitchTags = new Regex(@"</?switch>", RegexOptions.Compiled);
        private readonly Regex rxClipPathElement = new Regex(@"<(?<tag>clipPath)[^>]*>.*?</\k<tag>>",
                                                                  RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);

        private readonly Regex rxXMLPreserveSpaceAttribute = new Regex("xml:space=\"preserve\"", RegexOptions.IgnoreCase | RegexOptions.Compiled);
		private readonly CleanSVG.StatusMessager StatusMessage;
		private int _stylesReplaced;

		public SVGCleaner(CleanSVG.StatusMessager messager)
		{
			StatusMessage = messager;
		}

		private string DynamicStylesAsBlock
		{
			get
			{
				var sb = new StringBuilder();

				foreach (var keyValuePair in _dynamicStyles)
					sb.AppendLine("." + keyValuePair.Value + " { " + keyValuePair.Key + " }");

				return Environment.NewLine
				       + "<style type=\"text/css\">" + Environment.NewLine
				       + "<![CDATA[" + Environment.NewLine
				       + sb
				       + "]]>" + Environment.NewLine
				       + "</style>";
			}
		}

        private class SVGFragment
        {
            public readonly int Index;
            public readonly int Length;
            public string CleanedSVG;

            public SVGFragment(int index, int length)
            {
                Index = index;
                Length = length;
                CleanedSVG = null;
            }
        }

		public void Clean(string filePath, SVGCleanOptions options)
		{
            StatusMessage("Reading {0}...", filePath);

			string fileText;
			try
			{
				fileText = File.ReadAllText(filePath, Encoding.UTF8);
			}
			catch (Exception fe)
			{
				StatusMessage("Error {0} reading {1}.", fe.Message, filePath);
				return;
			}

			var originalLength = fileText.Length;
			StatusMessage("Original file is {0:#,##0} characters long.", originalLength);

            //  normalize line ends to our our scheme
		    fileText = Regex.Replace(fileText, @"(?<!\r)\n", Environment.NewLine, RegexOptions.Compiled);

			//	remove svg namespace prefixes and declaration
			var rxSvgPrefixDeclaration = new Regex(@"<svg:svg[^>]*?\sxmlns:svg=""http://www.w3.org/2000/svg""", RegexOptions.Compiled);
			if (rxSvgPrefixDeclaration.IsMatch(fileText))
			{
				var prefixRemoved = 0;
				fileText = rxSvgPrefixDeclaration.Replace(fileText, m => m.Value.Replace(":svg=", "="), 1);
				fileText = Regex.Replace(fileText, @"(?<=</?|\s)svg:",
				                         delegate
				                         	{
				                         		++prefixRemoved;
				                         		return string.Empty;
				                         	}
				                         , RegexOptions.Compiled);
				StatusMessage("\"svg:\" namespace prefix removed from {0:#,##0} elements.", prefixRemoved);
			}

			//  pull out large images encoded as data URIs
            if (options.ExtractLargeImages)
			    fileText = ExtractLargeImages(fileText, filePath);

		    var svgFragments = LocateSVGFragments(fileText);
            StatusMessage("File contains {0:#,##0} independent SVG fragment{1}.", svgFragments.Count, svgFragments.Count == 1 ? string.Empty : "s");
		    CleanSVGFragments(fileText, svgFragments, options);
		    fileText = AssembleSVGFragments(fileText, svgFragments);

		    var cleanedLength = fileText.Length;
			StatusMessage(Environment.NewLine + "Cleaned file is {0:#,##0} characters long ({1}% of original size)." + Environment.NewLine, cleanedLength, Math.Round(cleanedLength * 100D / originalLength, 1));

			var outputFilePath = filePath + ".clean" + Path.GetExtension(filePath);
			try
			{
				File.WriteAllText(outputFilePath, fileText, Encoding.UTF8);
			}
			catch (Exception fe)
			{
				StatusMessage("Error {0} writing {1}.", fe.Message, outputFilePath);
                Debug.Print(fe.Message + ": " + outputFilePath);
				return;
			}
		}

        private static string DecodeNamedEntities(Match m)
        {
            if (_ned == null)
                _ned = new NamedEntitiesDictionary();

            var name = m.Groups["name"].Value;

            //  don't encode the xml entities
            if (name == "lt" || name == "gt" || name == "amp" || name == "quot" || name == "apos")
                return m.Value;

            char unicodeValue;
            return _ned.TryGetValue(name, out unicodeValue) ? Convert.ToString(unicodeValue) : m.Value;
        }

        private static string DecodeNumericEntites(Match m)
        {
            var numericValue = m.Value.ToLower().StartsWith("&#x")
                                   ? Convert.ToUInt16(m.Value.Substring(3, m.Value.Length - 4), 16)
                                   : Convert.ToUInt16(m.Value.Substring(2, m.Value.Length - 3));

            //  don't encode the xml entities
            switch (numericValue)
            {
                case '<':
                    return "&lt;";
                case '>':
                    return "&gt;";
                case '&':
                    return "&amp;";
                case '\'':
                    return "&apos;";
                case '"':
                    return "&quot;";
                default:
                    return Convert.ToString(Convert.ToChar(numericValue));
            }
        }

	    private static string DecodeEntities(string s)
        {
            s = Regex.Replace(s, @"&amp;(?=[a-zA-Z]+;)", "&");    // convert &amp; that precedes entities
            s = Regex.Replace(s, @"&(?<name>[a-zA-Z]+);", DecodeNamedEntities); // convert named entities (but not lt gt amp)
            s = Regex.Replace(s, @"(&#\d+;)|(&#[xX][0-9A-Fa-f]+;)", DecodeNumericEntites); // convert numeric entities
            return s;
        }

        private string syntheticStylePrefix = "c";

        private void CleanSVGFragments(string fileText, List<SVGFragment> svgFragments, SVGCleanOptions options)
	    {
	        var nFrag = 0;
            foreach (var fragment in svgFragments)
            {
                if (svgFragments.Count > 1)
                {
                    StatusMessage(Environment.NewLine + "Cleaning SVG fragment {0}...", ++nFrag);
                    syntheticStylePrefix = string.Format("svg{0}c", nFrag);
                }
                else
                {
                    StatusMessage(string.Empty);
                    syntheticStylePrefix = "c";
                }

                fragment.CleanedSVG = CleanSVGFragment(fileText.Substring(fragment.Index, fragment.Length), options);
            }
	    }

	    private static string AssembleSVGFragments(string fileText, IEnumerable<SVGFragment> svgFragments)
	    {
	        var sb = new StringBuilder();

	        var currentIndex = 0;
	        foreach (var fragment in svgFragments)
	        {
                if (fragment.Index > currentIndex)
                    sb.Append(fileText.Substring(currentIndex, fragment.Index - currentIndex));

                sb.Append(Environment.NewLine).AppendFormat("<!-- SVG fragment cleaned by {0} on {1} -->", Program.TheMainForm.ProductName, DateTime.Now).Append(Environment.NewLine);
	            sb.Append(fragment.CleanedSVG).Append(Environment.NewLine);
	            currentIndex = fragment.Index + fragment.Length;
	        }

            if (currentIndex < fileText.Length)
                sb.Append(fileText.Substring(currentIndex));

	        return sb.ToString();
	    }

	    private static List<SVGFragment> LocateSVGFragments(string fileText)
        {
			var fragments = new List<SVGFragment>();

            var nest = 0;
            var fragmentStart = 0;
            for (var m = Regex.Match(fileText, @"<svg(?:\s|>)|</svg>", RegexOptions.Compiled); m.Success; m = m.NextMatch())
            {
                if (m.Value.StartsWith("<svg"))
                {
                    if (nest == 0)
                        fragmentStart = m.Index;

                    ++nest;
                }
                else if (nest > 0)
                {
                    --nest;

                    if (nest == 0)
                        fragments.Add(new SVGFragment(fragmentStart, m.Index + m.Length - fragmentStart));
                }
                else
                {
                    throw new ArgumentException("SVG file has too many </svg> closing tags.");
                }
            }

            if (nest != 0)
                throw new ArgumentException("SVG file has too few </svg> closing tags.");

            return fragments;
        }

        private string CleanSVGFragment(string svg, SVGCleanOptions options)
	    {
            if (!Regex.IsMatch(svg, @"^<svg\s.*</svg>$", RegexOptions.Compiled | RegexOptions.Singleline))
            {
                var sampleSize = Math.Min(20, svg.Length);

                StatusMessage("Invalid SVG fragment passed to CleanSVGFragment. Starts with \"{0}\", ends with \"{1}\".", 
                    svg.Substring(0, sampleSize), svg.Substring(svg.Length - sampleSize));

                return svg;
            }

            //  we don't like entities much; convert them to unicode characters (does not convert the 5 xml-recognized entities)
            svg = DecodeEntities(svg);

            //  self-close elements without content
            svg = SelfCloseElements(svg);

            if (options.InsertXlinkNamespace)
	            svg = InsertXlinkNamespace(svg);

            if (options.RemoveForeignNamespaceStuff)
	            svg = RemoveForeignNamespaceStuff(svg);

			if (options.RemoveViewboxDirective)
				svg = RemoveViewboxDirective(svg);

            if (options.RemoveComments)
	            svg = RemoveComments(svg);

			if (options.RemoveMetadataBlocks)
				svg = RemoveMetadata(svg); 

            if (options.RemoveSyntheticTitles)
	            svg = RemoveSyntheticTitles(svg);

            if (options.RemoveAllTitles)
                svg = RemoveAllTitles(svg); 

            if (options.RemoveAllDescs)
                svg = RemoveAllDescs(svg);

            if (options.RemoveAllForeignObjects)
                svg = RemoveAllForeignObjects(svg);

            if (options.RemoveAllSwitchTags)
                svg = RemoveAllSwitchTags(svg);

            if (options.RemoveClipPaths)
                svg = RemoveClipPaths(svg);

            if (options.RemoveFilters)
                svg = RemoveFilters(svg);

            if (options.RemoveFonts)
                svg = RemoveFonts(svg);

            if (options.ReduceFloatingPointPrecision)
	            svg = ReduceFloatingPointPrecision(svg, options.DigitsOfFloatingPointPrecision);

            if (options.RemoveUnreferencedIDs)
	            svg = RemoveUnreferencedIds(svg);

            if (options.AddOverflowVisibleToMarkers)
                svg = AddOverflowVisibleToMarkers(svg);

            if (options.ConvertXMLSpacePresrve)
                svg = svg.Replace(@"xml:space=""preserve""", @"style=""white-space:pre"" xml:space=""preserve""");

            svg = svg.Replace(@"stroke-width:0.0", @"stroke-width:0.1");

            if (options.ConvertAttributesToStyle)
	            svg = ConvertAttributesToStyle(svg);

            if (options.ConvertStyleToClass)
	            svg = ConvertStyleToClass(svg);

            if (options.CollapseMultipleClassAttributes)
	            svg = CollapseMultipleClassAttributes(svg);

            if (options.RemoveSymbolElementsWithoutIDs)
	            svg = RemoveSymbolsWithoutIds(svg);

            if (options.ConvertCircularEllipsesToCircles)
	            svg = SVGElementCleaners.EllipsesToCircles(svg, StatusMessage);

            if (options.ConvertSimplePathsToBasicShapes)
	            svg = SVGElementCleaners.PathsToPolylines(svg, StatusMessage);


                


        	var styleRules = string.Empty;
			if (options.ConvertTspansToText)
			{
				styleRules = CollectAllStyleRules(svg);
				svg = SVGElementCleaners.ConvertTspans(svg, styleRules, StatusMessage);
			}

        	if (options.RemoveExcessSpaceInPathData)
	            svg = RemoveExcessSpaceInPathData(svg);

            if (options.RemoveEmptyStructureElements)
                svg = RemoveEmptyStructureElements(svg);

            if (options.RemoveUnneededGroupElements)
	            svg = RemoveUnneededGroupElements(svg);

			if (options.ConvertTspansToText)
				svg = SVGElementCleaners.CollapseCollabsibleTextRuns(svg, styleRules, StatusMessage);

            if (options.CleanUpWhitespace)
	            svg = CleanUpWhitespace(svg);

            if (options.BreakUpSuperLongPaths)
                svg = BreakUpSuperLongPaths(svg);

            if (options.WrapLongPathsPolygonsAndPolylines)
                svg = WrapLongPathElements(svg);

	        return svg;
	    }

        private readonly Regex rxCloseableElement = new Regex(@"<(?<tag>\w+)(?<attrs>[^>]*)>\s*</\k<tag>>", RegexOptions.Singleline | RegexOptions.Compiled);

        private string SelfCloseElements(string svg)
        {
            var elementsSelfClosed = 0;
            svg = rxCloseableElement.Replace(svg, delegate(Match m)
            {
                ++elementsSelfClosed;
                return "<" + m.Groups["tag"].Value + m.Groups["attrs"].Value + " />";
            });

            if (elementsSelfClosed > 0)
                StatusMessage("{0:#,##0} elements without content self-closed.", elementsSelfClosed);

            return svg;
        }

	    private string AddOverflowVisibleToMarkers(string svg)
	    {
            var attributesAdded = 0;
            svg = Regex.Replace(svg, @"<marker\s+", delegate(Match m)
                                                        {
                                                            ++attributesAdded;
                                                            return m.Value + "overflow=\"visible\" ";
                                                        });
            if (attributesAdded > 0)
                StatusMessage("{0:#,##0} overflow=\"visible\" attributes added to marker elements.", attributesAdded);

	        return svg;
        }

	    private string RemoveClipPaths(string svg)
        {
            var clipAttributesRemoved = 0;
            svg = Regex.Replace(svg, @"\s+clip[\w\-]*\s*=\s*""[^""]*""", delegate
                                                                            {
                                                                                ++clipAttributesRemoved;
                                                                                return string.Empty;
                                                                            });
            if (clipAttributesRemoved > 0)
                StatusMessage("{0:#,##0} clip-related attributes removed.", clipAttributesRemoved);

            var clipStylePropsRemoved = 0;
            svg = Regex.Replace(svg, @"(?<=[{;\s])clip[\w\-]*:[^;}""']*", delegate
                                                                           {
                                                                               ++clipStylePropsRemoved;
                                                                               return string.Empty;
                                                                           });
            if (clipStylePropsRemoved > 0)
                StatusMessage("{0:#,##0} clip-related style properties removed.", clipStylePropsRemoved);

            var clipPathsRemoved = 0;
            svg = rxClipPathElement.Replace(svg, delegate
                                                     {
                                                         ++clipPathsRemoved;
                                                         return string.Empty;
                                                     });

            if (clipPathsRemoved > 0)
                StatusMessage("{0:#,##0} clipPath elements removed.", clipPathsRemoved);

            return svg;
        }

        private string RemoveFilters(string svg)
        {
            var filterAttributesRemoved = 0;
            svg = Regex.Replace(svg, @"\s+filter[\w\-]*\s*=\s*""[^""]*""", delegate
            {
                ++filterAttributesRemoved;
                return string.Empty;
            });
            if (filterAttributesRemoved > 0)
                StatusMessage("{0:#,##0} filter-related attributes removed.", filterAttributesRemoved);

            var filterStylePropsRemoved = 0;
            svg = Regex.Replace(svg, @"(?<=[{;\s])filter[\w\-]*:[^;}""']*", delegate
            {
                ++filterStylePropsRemoved;
                return string.Empty;
            });
            if (filterStylePropsRemoved > 0)
                StatusMessage("{0:#,##0} filter-related style properties removed.", filterStylePropsRemoved);

            var filtersRemoved = 0;
            svg = rxFilterElement.Replace(svg, delegate
            {
                ++filtersRemoved;
                return string.Empty;
            });

            if (filtersRemoved > 0)
                StatusMessage("{0:#,##0} filter elements removed.", filtersRemoved);

            return svg;
        }

	    //	if we have a viewBox directive, remove it and put in a width and height equal to the width and height of the viewBox
		private string RemoveViewboxDirective(string svg)
		{
			return rxSvg.Replace(svg, delegate (Match m)
			                          	{
			                          		var replacement = m.Value;

			                          		var rxViewBox = new Regex(@"\s+viewBox\s*=\s*""0\s+0\s+(?<vbw>[0-9.]+)\s+(?<vbh>[0-9.]+)""");
											var viewBoxMatch = rxViewBox.Match(replacement);
											if (viewBoxMatch.Success)
											{
												replacement = Regex.Replace(replacement, @"\s+(width|height)\s*=\s*""[^""]*""", string.Empty);
												replacement = rxViewBox.Replace(replacement, string.Format(" width=\"{0}\" height=\"{1}\"",
												                                                           viewBoxMatch.Groups["vbw"].Value,
												                                                           viewBoxMatch.Groups["vbh"].Value));
											}

											return replacement;
			                          	}, 1);
		}

		protected string CollectAllStyleRules(string svg)
	    {
                var sb = new StringBuilder();

                for (var m = rxStyleCdata.Match(svg); m.Success; m = m.NextMatch())
                    sb.Append(m.Groups["inner"].Value);

	            return sb.ToString();
	    }

	    private string RemoveSymbolsWithoutIds(string svg)
	    {
	        var rxSymbolNoId = new Regex(@"<symbol(?:(?!\sid\s*=)[^>])*>.*?</symbol>", RegexOptions.Singleline | RegexOptions.Compiled);

	        var removedSymbols = 0;

	        svg = rxSymbolNoId.Replace(svg, delegate
	                                            {
	                                                ++removedSymbols;
	                                                return string.Empty;
	                                            }
	                                            );

            if (removedSymbols > 0)
                StatusMessage("{0:#,##0} symbol elements without IDs removed.", removedSymbols);

            return svg;
        }

        //  can do better by wrapping only in the path/polyline/polygon data
	    private string WrapLongPathElements(string svg)
	    {
	        var startLength = svg.Length;
	        var longPathsWrapped = 0;
            var rxLongPath = new Regex(@"(?<=" + Environment.NewLine + @")<(?:path|polygon|polyline)\s[^>]{120,}>(?=" + Environment.NewLine + @")", RegexOptions.Compiled);
	    	var rxBreakPoint = new Regex(@".{110}[^ ]*(?= )", RegexOptions.Compiled);
	        svg = rxLongPath.Replace(svg,
	                                  delegate(Match m)
	                                      {
	                                          ++longPathsWrapped;
											  return rxBreakPoint.Replace(m.Value, "$&" + Environment.NewLine);
	                                      });

            if (longPathsWrapped > 0)
                StatusMessage("{0:#,##0} long elements wrapped to multiple lines adding {1:#,##0} characters.", longPathsWrapped, svg.Length - startLength);

	        return svg;
        }

        private string BreakUpSuperLongPaths(string svg)
        {
            var startLength = svg.Length;
            var superLongPathsBroken = 0;
            var nNewPathsCreated = 0;
            var rxSuperLongPath =
                new Regex(
                    @"(?<=" + Environment.NewLine +
                    @")(?<pre><path\s[^>]*?d\s*=\s*"")(?<inner>[0-9\. ,+\-mlhvcsqtaz]{1000,})(?<post>""[^>]*?>)",
                    RegexOptions.IgnoreCase | RegexOptions.Compiled);
            var rxZM = new Regex("(?<=[zZ])M", RegexOptions.Compiled);

            svg = rxSuperLongPath.Replace(svg,
                                          delegate(Match m)
                                              {
                                                  ++superLongPathsBroken;
                                                  return m.Groups["pre"].Value + rxZM.Replace(m.Groups["inner"].Value, delegate(Match zm)
                                                                                                                          {
                                                                                                                              ++nNewPathsCreated;
                                                                                                                              return m.Groups["post"].Value +
                                                                                                                                     Environment.NewLine +
                                                                                                                                     m.Groups["pre"].Value + "M";
                                                                                                                          }) + m.Groups["post"].Value;
                                              });

            if (superLongPathsBroken > 0)
                StatusMessage("{0:#,##0} super long paths broken into {1:#,##0} smaller paths adding {2:#,##0} characters.", superLongPathsBroken, nNewPathsCreated, svg.Length - startLength);

            return svg;
        }

	    private static string RemoveExcessSpaceInPathData(string svg)
	    {
            var rxDataAttribute = new Regex(@"d\s*=\s*""(?<inner>[0-9\. ,+\-mlhvcsqtaz]+)""", RegexOptions.IgnoreCase | RegexOptions.Compiled);
            var rxExcessSpace = new Regex(@"\s+(?<command>[mlhvcsqtaz])\s*|\s*(?<command>[mlhvcsqtaz])\s+|\s+(?<command>"")$", RegexOptions.IgnoreCase | RegexOptions.Compiled);

	        return rxDataAttribute.Replace(svg, m => rxExcessSpace.Replace(m.Value, "${command}"));
	    }

	    private string CollapseMultipleClassAttributes(string fileText)
		{
			var rxElementWithClassAttrs = new Regex(@"<\w+\s+(?<classes>[^>]*?class\s*=\s*""(?<classname>[^""]+)""){2,}[^>]*>");

			var elements = 0;
			var classAttrs = 0;

			fileText = rxElementWithClassAttrs.Replace(fileText,
								delegate(Match m)
									{
										var nClasses = m.Groups["classname"].Captures.Count;

										++elements;
										classAttrs += nClasses;

										var classnames = new string[nClasses];
										for (var i = 0; i < nClasses; ++i)
											classnames[i] = m.Groups["classname"].Captures[i].Value;

										var replacement = Regex.Replace(m.Value, @"\s+class\s*=\s*""[^""]+""", string.Empty);
										replacement = Regex.Replace(replacement, @"^<\w+\s+", "$&class=\"" + string.Join(" ", classnames) + "\" ");
										return replacement;
									}
				);

			if (elements > 0)
				StatusMessage("{0:#,##0} elements containing {1:#,##0} class attributes collapsed to one class attribute per element.", elements, classAttrs);

			return fileText;
		}

		private string ConvertAttributesToStyle(string fileText)
		{
			var rxPresentationAttributes = new Regex(@"(?<attrs>\s+(?<key>(fill|stroke|font|text|clip|filter|opacity|marker|overflow)(?:-[a-z]+)?)\s*=\s*""(?<value>[^""]+)"")+");

			var attrsReplaced = 0;
			var styleReplacements = 0;

			fileText = rxPresentationAttributes.Replace(fileText,
				delegate(Match m)
					{
						var sb = new StringBuilder(" style=\"");

						for (var i = 0; i < m.Groups["attrs"].Captures.Count; ++i)
							sb.AppendFormat("{0}:{1};", m.Groups["key"].Captures[i].Value, m.Groups["value"].Captures[i].Value);

						sb.Append("\"");

						attrsReplaced += m.Groups["attrs"].Captures.Count;
						++styleReplacements;

						return sb.ToString();
					}
				);

			if (attrsReplaced > 0)
				StatusMessage("{0:#,##0} presentation attributes replaced with {1:#,##0} style attributes.", attrsReplaced, styleReplacements);

			return fileText;
		}

		private string RemoveComments(string fileText)
		{
            //  save a lot of work if there are no comments
            if (!Regex.IsMatch(fileText, @"<!--.*?-->", RegexOptions.Compiled | RegexOptions.Singleline))
                return fileText;

			var commentsRemoved = 0;

		    var nest = 0;
		    var currentIndex = 0;
            var commentStartIndex = 0;
            var sb = new StringBuilder();
            for (var m = Regex.Match(fileText, @"<!--|-->", RegexOptions.Compiled); m.Success; m = m.NextMatch())
            {
                if (m.Value == "<!--")
                {
                    if (nest == 0)
                        commentStartIndex = m.Index;

                    ++nest;
                }
                else if (nest > 0)
                {
                    --nest;

                    if (nest == 0)
                    {
                        sb.Append(fileText.Substring(currentIndex, commentStartIndex - currentIndex));
                        currentIndex = m.Index + m.Length;
                        ++commentsRemoved;
                    }
                }
            }

            if (currentIndex < fileText.Length)
		        sb.Append(fileText.Substring(currentIndex));

			if (commentsRemoved > 0)
				StatusMessage("{0:#,##0} comments removed.", commentsRemoved);

			return sb.ToString();
		}

		private string ReduceFloatingPointPrecision(string fileText, int digits)
		{
			//	only look for digits in quoted attributes
			var rxNumericAttributes = new Regex(@"(?<attr>[\w\-]+)\s*=\s*""(?<inner>(?:matrix\()?[0-9e\. ,+\-mlhvcsqtaz]+\)?)""", RegexOptions.IgnoreCase | RegexOptions.Compiled);

			//  convert exponential notation and anything that uses more digits than we need
			var rxFloatingPoint = new Regex(@"(?:\+|-)?\d+\.(?:\d+e(?:\+|-)\d+|\d{" + (digits + 1) + @",})", RegexOptions.Compiled);

			var valuesAdjusted = 0;

			fileText = rxNumericAttributes.Replace(fileText, delegate(Match am)
			                                                 	{
			                                                 		var inner = rxFloatingPoint.Replace(am.Groups["inner"].Value,
			                                                 		                                    delegate(Match m)
			                                                 		                                    	{
			                                                 		                                    		++valuesAdjusted;
			                                                 		                                    		return Math.Round(double.Parse(m.Value), digits).ToString();
			                                                 		                                    	});
			                                                 		return string.Format("{0}=\"{1}\"", am.Groups["attr"].Value, inner);
			                                                 	});

			if (valuesAdjusted > 0)
				StatusMessage("{0:#,##0} values reduced to {1} decimal places.", valuesAdjusted, digits);

			return fileText;
		}

		private string InsertXlinkNamespace(string fileText)
		{
			//  patch the first svg element to contain an xlink namespace declaration if it doesn't already have one
			return rxSvg.Replace(fileText, delegate(Match m)
			                               	{
			                               		if (m.Value.Contains("xmlns:xlink="))
			                               			return m.Value;

												StatusMessage("Added xlink XML namespace declaration.");

			                               		return m.Value.Substring(0, 4) + " xmlns:xlink=\"http://www.w3.org/1999/xlink\"" +
			                               		       m.Value.Substring(4);
			                               	}, 1);
		}

		private string ConvertStyleToClass(string fileText)
		{
			_stylesReplaced = 0;
			_dynamicStyles.Clear();
			fileText = rxStyleAttribute.Replace(fileText, StyleToClass);

            if (_dynamicStyles.Count == 0)
                return fileText;

			const string styleCloseTag = "</style>";
			var insertAt = fileText.LastIndexOf(styleCloseTag);
			if (insertAt != -1)
				insertAt += styleCloseTag.Length;
			else
			{
				var m = rxSvg.Match(fileText);

				if (m.Success)
					insertAt = m.Index + m.Length;
				else
					insertAt = fileText.IndexOf('>');
			}

			if (_stylesReplaced > 0)
				StatusMessage("{0:#,##0} style attributes replaced with {1:#,##0} class references.", _stylesReplaced, _dynamicStyles.Count);

			return fileText.Substring(0, insertAt) + DynamicStylesAsBlock + fileText.Substring(insertAt);
		}

		private string StyleToClass(Match m)
		{
			var css = NormalizeCssProperties(m.Groups["inner"].Value);

			if (!_dynamicStyles.ContainsKey(css))
                _dynamicStyles.Add(css, syntheticStylePrefix + _dynamicStyles.Count);

			++_stylesReplaced;

			return string.Format(" class=\"{0}\"", _dynamicStyles[css]);
		}

		//	add code to fix unitless font-size and stroke-width
		private string ReformatStyleBlock(Match m)
		{
			var sb = new StringBuilder();

            //  assumes we don't use braces within CSS style rules
			var rules = m.Groups["inner"].Value.Trim(" /*\t\r\n".ToCharArray()).Split('}');

			foreach (string r in rules)
			{
				var trimmed = r.Trim();

				if (trimmed == string.Empty)
					continue;

				var halves = trimmed.Split('{');
				Debug.Assert(halves.Length == 2, "Invalid CSS rule in ReformatStyleBlock");
				var selector = halves[0].Trim();
				var properties = NormalizeCssProperties(halves[1]);
				sb.AppendLine(selector + " { " + properties + " }");
			}

			return "<style type=\"text/css\">" + Environment.NewLine
			       + "/*<![CDATA[*/" + Environment.NewLine
			       + sb
			       + "/*]]>*/" + Environment.NewLine
			       + "</style>";
		}

        private readonly Regex rxCssPropNameAndValue = new Regex(@"\s*(?<propName>[\w\-]+)\s*:\s*(?<propValue>(?:(?:(?<q>['""]).*?\k<q>)|[^;])+)\s*;?", RegexOptions.Singleline | RegexOptions.Compiled);

		private string NormalizeCssProperties(string propertyList)
		{
			var propertyValues = new Dictionary<string, string>();

		    var namesAndValues = rxCssPropNameAndValue.Matches(propertyList);

			foreach (Match prop in namesAndValues)
			{
			    var propName = prop.Groups["propName"].Value;

				//  ignore css properties that are prefixes with a foreign namespace we ignored
				var ignore = false;
				foreach (string prefix in _foreignNamespacePrefixes)
				{
				    if (!propName.StartsWith("-" + prefix + "-"))
                        continue;

				    ignore = true;
				    break;
				}

			    if (ignore)
                    continue;

			    var propValue = prop.Groups["propValue"].Value.Trim();

			    //  reduce the precision of long high-precision numeric values. should use the options number of digits
			    //  of precision value but we don't have that handy right here unless we make it a global.
			    propValue = Regex.Replace(propValue, @"^(?<number>(?:\+|-)?\d+\.(?:\d+e(?:\+|-)\d+|\d{4,}))(?<units>[a-z]{2})?$",
			                              m => Math.Round(double.Parse(m.Groups["number"].Value), 3).ToString() + m.Groups["units"].Value);

                propertyValues[propName] = propValue;
			}

			var keys = new string[propertyValues.Keys.Count];
			propertyValues.Keys.CopyTo(keys, 0);
			Array.Sort(keys, (a, b) => Regex.Replace(a, @"^-\w+-", string.Empty).CompareTo(Regex.Replace(b, @"^-\w+-", string.Empty)));

			var sb = new StringBuilder();
			foreach (string key in keys)
				sb.AppendFormat("{0}: {1}; ", key, propertyValues[key]);

			propertyList = sb.ToString().Trim();

			// add pixel units to unitless font sizes and stroke widths
			propertyList = Regex.Replace(propertyList, @"(font-size|stroke-width):\s*[\d\.]+(?=;)", "$&px");

			return propertyList;
		}

		private string CleanUpWhitespace(string fileText)
		{
            //var startLength = fileText.Length;

            ////	clean up the white space mess we've made (could add code to preserve white space in and around <text> and <tspan>)
            //fileText = Regex.Replace(fileText, @"(?<!\\)[ \r\n\t]{2,}", " ", RegexOptions.Singleline | RegexOptions.Compiled);	// don't use \s because it includes the non-breaking space \u00A0
            //fileText = Regex.Replace(fileText, @"\s+>", ">", RegexOptions.Singleline | RegexOptions.Compiled);
            //fileText = Regex.Replace(fileText, @"\s*/>", " />", RegexOptions.Singleline | RegexOptions.Compiled);
            //fileText = Regex.Replace(fileText, @">\s*<(?!tspan|/text)", ">" + Environment.NewLine + "<", RegexOptions.Singleline | RegexOptions.Compiled);

            ////  clean up the mess the above may have made with style blocks            
            //fileText = CleanUpStyleBlocks(fileText);

            //var endLength = fileText.Length;

            //if (endLength != startLength)
            //    StatusMessage("{0:#,##0} characters {1} by cleaning up the whitespace.", Math.Abs(endLength - startLength), endLength < startLength ? "saved" : "added");

            return fileText;
		}

	    private string CleanUpStyleBlocks(string fileText)
	    {
	        fileText = rxStyleCdata.Replace(fileText, ReformatStyleBlock);
	        return fileText;
	    }

	    private string RemoveEmptyStructureElements(string fileText)
        {
            var emptyElementsRemoved = 0;
            var rxEmptyStructureElement = new Regex(@"<(g|defs|metadata|text|tspan)[^>]*?/>|<(?<tag>g|defs|metadata|text|tspan)(?:\s+[^>]*)?>\s*</\k<tag>>", RegexOptions.Compiled);
            fileText = rxEmptyStructureElement.Replace(fileText, delegate
                                                                     {
                                                              ++emptyElementsRemoved;
                                                              //return "<!--" + m.Value + "-->";
                                                              return string.Empty;
                                                          });

            if (emptyElementsRemoved > 0)
                StatusMessage("{0:#,##0} empty structure elements removed.", emptyElementsRemoved);

            return fileText;
        }

	    //	remove group elements with no attributes since they do nothing for rendering.
        //  these may have been created because we removed foreign namespaced attributes or unreferenced ids.
		private string RemoveUnneededGroupElements(string fileText)
		{
			var uselessGroupsRemoved = 0;
            var rxGroupStartEnd = new Regex(@"<g(?:\s|>)|</g>", RegexOptions.Compiled);
		    var deleteMatchingEnd = new Stack<bool>();
            fileText = rxGroupStartEnd.Replace(fileText, delegate(Match m)
			                                            	{
                                                                if (m.Value == "</g>")
                                                                {
                                                                    if (deleteMatchingEnd.Pop())
                                                                    {
                                                                        ++uselessGroupsRemoved;
                                                                        return string.Empty;
                                                                    }

                                                                    return m.Value;
                                                                }

                                                                //  push a boolean indicating whether this group has no attributes
			                                            	    deleteMatchingEnd.Push(Regex.IsMatch(m.Value, @"<g\s*>"));

                                                                //  if the group start has no attributes, remove it
			                                            	    return deleteMatchingEnd.Peek() ? string.Empty : m.Value;
			                                            	});

			if (uselessGroupsRemoved > 0)
				StatusMessage("{0:#,##0} groups without attributes removed.", uselessGroupsRemoved);

			return fileText;
		}

		//	visio and inkscape generate synthetic titles that are nothing more than a noun describing the shape and a numeric suffix
		private string RemoveSyntheticTitles(string fileText)
		{
			var syntheticTitlesRemoved = 0;

			fileText =  rxSyntheticTitles.Replace(fileText, delegate
			                                          	{
			                                          		++syntheticTitlesRemoved;
			                                          		return string.Empty;
			                                          	});

			if (syntheticTitlesRemoved > 0)
				StatusMessage("{0:#,##0} synthetic titles removed.", syntheticTitlesRemoved);

			return fileText;
		}

        private string RemoveMetadata(string fileText)
        {
            var metadatasRemoved = 0;

            fileText = rxMetadataElement.Replace(fileText, delegate
            {
                ++metadatasRemoved;
                return string.Empty;
            });

            if (metadatasRemoved > 0)
                StatusMessage("{0:#,##0} metadata elements removed.", metadatasRemoved);

            return fileText;
        }

        private string RemoveFonts(string fileText)
        {
            var fontsRemoved = 0;

            fileText = rxFontElement.Replace(fileText, delegate
            {
                ++fontsRemoved;
                return string.Empty;
            });

            if (fontsRemoved > 0)
                StatusMessage("{0:#,##0} font elements removed.", fontsRemoved);

            return fileText;
        }

		private string RemoveAllTitles(string fileText)
		{
			var titlesRemoved = 0;

			fileText = rxTitleElement.Replace(fileText, delegate
			{
				++titlesRemoved;
				return string.Empty;
			});

			if (titlesRemoved > 0)
				StatusMessage("{0:#,##0} title elements removed.", titlesRemoved);

			return fileText;
		}

        private string RemoveAllDescs(string fileText)
        {
            var descsRemoved = 0;

            fileText = rxDescElement.Replace(fileText, delegate
            {
                ++descsRemoved;
                return string.Empty;
            });

            if (descsRemoved > 0)
                StatusMessage("{0:#,##0} desc elements removed.", descsRemoved);

            return fileText;
        }

        private string RemoveAllForeignObjects(string fileText)
        {
            var foreignObjectsRemoved = 0;

            fileText = rxForeignObjectElement.Replace(fileText, delegate
            {
                ++foreignObjectsRemoved;
                return string.Empty;
            });

            if (foreignObjectsRemoved > 0)
                StatusMessage("{0:#,##0} foreignObject elements removed.", foreignObjectsRemoved);

            return fileText;
        }

        private string RemoveAllSwitchTags(string fileText)
        {
            var switchsRemoved = 0;

            fileText = rxSwitchTags.Replace(fileText, delegate
            {
                ++switchsRemoved;
                return string.Empty;
            });

            if (switchsRemoved > 0)
                StatusMessage("{0:#,##0} switch start and end tags removed.", switchsRemoved / 2);

            return fileText;
        }

		private string RemoveUnreferencedIds(string fileText)
		{
			var referencedIds = new Dictionary<string, object>();

            //  always include the id of our top-most svg element if it has one
		    var svgId = Regex.Match(fileText, @"^<svg\s[^>]*id\s*=\s*""(?<id>[^""]+)""");
            if (svgId.Success)
            {
                referencedIds.Add(svgId.Groups["id"].Value, null);

                //  and include this id used by the svg drag and zoom code if our svg element has an id
                referencedIds.Add("svgscale", null);
            }

		    for (var m = rxLocalIdReference.Match(fileText); m.Success; m = m.NextMatch())
				if (!referencedIds.ContainsKey(m.Groups["id"].Value))
					referencedIds.Add(m.Groups["id"].Value, null);

			var idsRemoved = 0;

			fileText = rxIdAttribute.Replace(fileText, delegate(Match m)
			                                           	{
			                                           		if (referencedIds.ContainsKey(m.Groups["id"].Value))
																return m.Value;

			                                           		++idsRemoved;
			                                           		return string.Empty;
			                                           	});

			if (idsRemoved > 0)
				StatusMessage("{0:#,##0} unreferenced id attributes removed; {1:#,##0} referenced ids remain.", idsRemoved, referencedIds.Count);

			return fileText;
		}

		//	foreign namespaces are all but svg and xlink

		private string RemoveForeignNamespaceStuff(string fileText)
		{
			_foreignNamespacePrefixes.Clear();

			//	collect all the namespace prefixes while we delete their declarations.
			fileText = rxForeignNamespace.Replace(fileText, delegate(Match m)
			                                                	{
			                                                		var p = m.Groups["prefix"].Value.ToLower();
                                                                    if (!_foreignNamespacePrefixes.Contains(p))
                                                                    {
                                                                        _foreignNamespacePrefixes.Add(p);
                                                                        StatusMessage(
                                                                            "Declaration of \"{0}\" XML namespace removed.",
                                                                            p);
                                                                    }
			                                                	    return string.Empty;
			                                                	});

			foreach (string prefix in _foreignNamespacePrefixes)
			{
				var foreignElementsRemoved = 0;

				//	take out the namespaced elements first as that leaves fewer attributes
				fileText = new Regex(@"<" + prefix + @":\w+[^>]*/>|<(?<tag>" + prefix + @":\w+)[^>]*>.*?</\k<tag>>",
									 RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled).Replace(fileText, delegate
									 {
										 ++foreignElementsRemoved;
										 return string.Empty;
									 });

				if (foreignElementsRemoved > 0)
					StatusMessage("{0:#,##0} elements in the \"{1}\" namespace removed.", foreignElementsRemoved, prefix);

				var foreignAttributesRemoved = 0;

				//	then take out the namespaced attributes
				fileText = new Regex(@"\s+" + prefix + @":[\w\-]+\s*=\s*""[^""]*""", RegexOptions.IgnoreCase | RegexOptions.Compiled).Replace(fileText, delegate
				{
					++foreignAttributesRemoved;
					return string.Empty;
				});

				if (foreignAttributesRemoved > 0)
					StatusMessage("{0:#,##0} attributes in the \"{1}\" namespace removed.", foreignAttributesRemoved, prefix);
			}

			return fileText;
		}

		private string ExtractLargeImages(string fileText, string filePath)
		{
			var writtenImages = new Dictionary<string, string>();
			var imageCount = 0;

			var newText = new StringBuilder();
			var fileTextIndex = 0;
			for (var m = Regex.Match(fileText, @"xlink:href=""(?<href>data:image/(?<fmt>png|jpeg|gif);base64,(?<data>[a-zA-Z0-9/+=\s]{1024,}))"""); m.Success; m = m.NextMatch())
			{
				var dataString = m.Groups["data"].Value;

				if (!writtenImages.ContainsKey(dataString))
				{
					var imageFileName = string.Format("{0}.image{1}.{2}", filePath, ++imageCount, m.Groups["fmt"].Value);
					File.WriteAllBytes(imageFileName, Convert.FromBase64String(dataString));
					writtenImages.Add(dataString, imageFileName);
					StatusMessage("Large image moved to file {0}.", imageFileName);
				}

				var hrefGroup = m.Groups["href"];
				newText.Append(fileText.Substring(fileTextIndex, hrefGroup.Index - fileTextIndex));
				newText.Append(HttpUtility.UrlPathEncode(Path.GetFileName(writtenImages[dataString])));
				fileTextIndex = hrefGroup.Index + hrefGroup.Length;
			}

			if (fileTextIndex > 0)
			{
				newText.Append(fileText.Substring(fileTextIndex));
				fileText = newText.ToString();
			}

			return fileText;
		}
	}
}